﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;
using Metrics.Metrics;
using Metrics.Utils;

namespace Metrics.Head
{
    class Getter
    {
        public static XmlDocument LogXmlDoc;

        /// <summary>
        /// Конструктор считывает Log
        /// </summary>
        public Getter()
        {
            if (LogXmlDoc != null) return;
            LogXmlDoc = new XmlDocument();
            var logFilePath = String.Format(@"{0}\log.xml", Program.LogsPath);

            try
            {
                LogXmlDoc.Load(logFilePath);
            }
            catch (Exception)
            {
                Console.WriteLine("Unable to open log file {0}", logFilePath);
                Environment.Exit(1);
            }
        }

        /// <summary>
        /// Метод определяет корректность запроса и наличие необходимых данных для вычисления метрики.
        /// </summary>
        public int GetMetricOfNode(string nodePath, int revNum, Metric metric)
        {
            #region Проверка существования ревизии запроса
            if (Xml.GetLogNode(revNum) == null)
            {
                Console.WriteLine("Revision {0} doesn't exist.", revNum);
                return -1;
            }
            #endregion

            #region Проверка на то, сушествовал ли узел и существует ли до сих пор
            var nodeLastNotation = Xml.FindLastNotation(revNum, nodePath);
            if (nodeLastNotation == 0)
            {
                Console.WriteLine("Node {0} didn't exist till {1} revision", nodePath, revNum);
                return -1;
            }

            var node = Xml.GetLogNode(nodeLastNotation, "[text()=\"/" + nodePath + "\"]");
            if (node != null) // null если директория, а не файл. обход исключения
                if (node.Attributes["action"].Value == "D")
                {
                    Console.WriteLine("Node {0} was deleted at {1} revision.", nodePath, nodeLastNotation);
                    return -1;
                }
            #endregion

            var files = new List<string>();
            GetFilesList(nodePath, revNum, ref files);
            FilterFilesList(ref files);

            #region Проверяем наличие необходимых diff'ов
            var lastNotations = new int[files.Count];
            var firstNotations = new int[files.Count];
            var lastRecords = new int[files.Count];
            int missDiffsLeft = revNum, missDiffsRight = 0;


            for (var i = 0; i < files.Count; i++)
            {
                lastNotations[i] = Xml.FindLastNotation(revNum, files[i]);
                firstNotations[i] = Xml.FindFirstNotation(0, files[i]);
                lastRecords[i] = Xml.FindLastRecord(lastNotations[i], firstNotations[i], files[i], metric.Name);

                for (var k = lastRecords[i]; k < lastNotations[i]; k++)
                {
                    var diffFileName = String.Format(@"{0}\{1}vs{2}.txt", Program.DiffsPath, k, k + 1); // формируем имя дифф-файла и проверяем его на наличие
                    if (File.Exists(diffFileName)) continue;
                    if (lastRecords[i] < missDiffsLeft) missDiffsLeft = lastRecords[i];
                    if (lastNotations[i] > missDiffsRight) missDiffsRight = lastNotations[i];
                    break;
                }
            }

            if (missDiffsRight > missDiffsLeft)
            {
                Console.WriteLine("Missing diff files! Please download diff file(s) from {0} to {1} revisions.", missDiffsLeft, missDiffsRight);
                return -1;
            }
            #endregion

            var metricValues = new int[files.Count];
            for (var i = 0; i < files.Count; i++)
                metricValues[i] = GetMetricOfFileAtRevision(files[i], lastNotations[i], metric);
            return metric.ConvergeMetrics(metricValues);
        }

        /// <summary>
        /// Возвращает значение метрики для файла в определенной ревизии
        /// </summary>
        public static int GetMetricOfFileAtRevision(string filePath, int revNum, Metric metric)
        {
            var revXmlDoc = new XmlDocument();
            var fileNotationInLog = Xml.FindLastNotation(revNum, filePath);
            var revXmlFile = String.Format(@"{0}\{1}.xml", Program.MetricsPath, fileNotationInLog);

            int metValue;
            var xPathToFileNode = Xml.CreateStringXpathToFile(filePath);

            if (!File.Exists(revXmlFile)) // создаем файл для записи данных
                File.WriteAllText(revXmlFile, "<metricsTree></metricsTree>");

            revXmlDoc.Load(revXmlFile); // загрузили текст из файла в xml-объект

            var fileNode = revXmlDoc.SelectSingleNode(xPathToFileNode);
            var metricNode =
                revXmlDoc.SelectSingleNode(String.Format("{0}/metric[@name=\"{1}\"]/text()", xPathToFileNode, metric.Name));
            // если нету пути к файлу - создаем его
            if (fileNode == null)
                fileNode = CreatePath(ref revXmlDoc, xPathToFileNode, "file");
            // если полный путь до узла с метрикой отсутствует - считаем
            // иначе метрика посчитана
            if (metricNode == null)
            {
                try
                {
                    metValue = metric.CountMetric(filePath, fileNotationInLog);
                    // добавляем к файлу узел-метрику
                    Xml.CreateNode(ref fileNode, "metric", metValue, new Hashtable { { "name", metric.Name } });
                    // сохраняем xml-дерево в файл
                    revXmlDoc.Save(revXmlFile);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    metValue = -1;
                }
            }
            else
            {
                try
                {
                    metValue = int.Parse(metricNode.Value);
                    // возможно исключение, если в узле нет значения или оно не число
                    //Console.WriteLine(
                    //    "{0} metric for {1} in {2} revision has been already calculated. It's value: {3}.",
                    //    metric.Name, filePath, revNum, metValue);
                }
                catch (Exception ex)
                {
                    Console.WriteLine("File {0} has defect:\n{1}", revXmlFile, ex.Message);
                    metValue = -1;
                }
            }
            Console.WriteLine("{0} metric for {1} in {2} revision is {3}.",
                              metric.Name, filePath, fileNotationInLog, metValue);
            return metValue;
        }

        ///<summary>
        /// Создает полный путь до узла соответсвующего файлу или директории.
        /// Возвращает лист в этом пути.
        ///</summary>
        private static XmlNode CreatePath(ref XmlDocument revXmlDoc, string filePath, string kindAttribute)
        {
            var nameFind = new Regex("@name=\"([^\"]+)\"", RegexOptions.IgnoreCase | RegexOptions.Compiled);
            var strWithNameOfAddNode = filePath.Substring(filePath.LastIndexOf('/'));
            var nameOfAddNode = nameFind.Match(strWithNameOfAddNode).Groups[1].Value;
            var filePathWithoutLastNode = filePath.Substring(0, filePath.LastIndexOf('/'));
            var parentOfCreateNode = revXmlDoc.SelectSingleNode(filePathWithoutLastNode) ??
                                     CreatePath(ref revXmlDoc, filePathWithoutLastNode, "dir");

            return Xml.CreateNode(ref parentOfCreateNode, "path", null,
                                  new Hashtable { { "name", nameOfAddNode }, { "kind", kindAttribute.Equals("file") ? "file" : "dir" } });
        }

        ///<summary>
        ///Дополняет список именами файлов, принадлежащих узлу
        ///</summary>
        private static void GetFilesList(string nodePath, int revNum, ref List<String> files)
        {
            if (Regex.IsMatch(nodePath, @"\.")) // елси узел - файл
                files.Add(nodePath);
            else
            {
                var candidateFiles = new Hashtable();

                for (var i = 1; i <= revNum; i++) // по всем ревизиям
                {
                    var fileNodes = Xml.GetLogNodes(i, "[@kind=\"file\" and (@action=\"A\" or @action = \"D\")]");
                    if (fileNodes != null)
                        foreach (XmlNode fileNode in fileNodes) // по всем добавленным файлам 
                        {
                            var filePath = fileNode.InnerText;
                            if (!Regex.IsMatch(filePath, "^/" + nodePath)) continue;
                            if (fileNode.Attributes["action"].Value == "A")
                                candidateFiles[filePath] = 1;
                            else
                                candidateFiles[filePath] = 0;
                        }
                }

                foreach (var key in candidateFiles.Keys)
                {
                    if ((int)candidateFiles[key] != 1) continue;
                    var internalFilePath = (string)key;
                    files.Add(internalFilePath.Substring(1));
                }
            }
        }

        private static void FilterFilesList(ref List<String> files)
        {
            var filteredFiles = new List<String>();
            bool excludeExt = false, excludeName = false;

            var extensionFilter = Program.ExtensionFilter.Trim();
            var nameFilter = Program.NameFilter.Trim();

            if (extensionFilter.EndsWith("!"))
            {
                extensionFilter = extensionFilter.Remove(extensionFilter.Length - 1);
                excludeExt = true;
            }

            if (nameFilter.EndsWith("!"))
            {
                nameFilter = nameFilter.Remove(nameFilter.Length - 1);
                excludeName = true;
            }

            extensionFilter = extensionFilter + "$";

            foreach (string fileName in files)
            {
                var include = 0;

                if (Regex.IsMatch(fileName, extensionFilter))
                {
                    if (!excludeExt)
                        include++;
                }
                else
                    if (excludeExt)
                        include++;

                if (Regex.IsMatch(fileName, nameFilter))
                {
                    if (!excludeName)
                        include++;
                }
                else
                    if (excludeName)
                        include++;

                if (include == 2) filteredFiles.Add(fileName);
            }

            files = filteredFiles;
        }
    }
}